using System;
//using System.Diagnostics;
using System.Linq;
using System.Security.Principal;
using System.Text;
using System.IO;
using System.Text.RegularExpressions;
using System.Collections;
using System.Collections.Generic;
using System.Configuration;
using System.Web;
using System.Web.Hosting;
using System.Web.UI;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Xsl;

using LynxWiki.DomainModel;
using LynxWiki.Repository;
using LynxWikiScripting;

namespace LynxWiki.Parser
{
    public class ParserEngine
    {

        private static ParserEngine _engine;
        private static ParserContext _context;
        private static TopicVersion _topic;
        private static string _appPath;
        private static WomDocument _womDocument;
        private static XslCompiledTransform _xslt;
        private static XsltDocument _xsltDoc;
        private static int chunkcnt;
        private const int _chunk = 4000; //this is used to optimize the regex matches by creatng strings 2000 bytes long to perform
                                       // matches on, rather than working on the whole document at one time

        private static string emailAddressString = @"([\w-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([\w-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)";
        private static Regex emailAddress = new Regex(emailAddressString, RegexOptions.Compiled);
        private static Regex externalWikiRef = new Regex(@"(\s)*(?<param>[\w\d\.]+)@(?<behavior>[\w\d]+([\w\d]{1,})+([\w\d\.])*)(\s)*",RegexOptions.Compiled);

        private static string multilinePreString = @"(?<PreBody>\r\n{@(?<KeyValue>[A-Z][a-zA-Z0-9]+)(?<PreText>.*?)\r\n}@(\k<KeyValue>+?))(?:\r\n)";
        private static Regex multilinePre = new Regex(multilinePreString, RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.CultureInvariant);

        private static string findEmoticonString = @"(\(A\)|\(a\))|(\(B\)|\(b\))|(\(C\)|\(c\))|(\(D\)|\(d\))|(\(E\)|\(e\))|(\(F\)|\(f\))|(\(G\)|\(g\))|(\(H\)|\(h\))|(\(I\)|\(i\))|(\(K\)|\(k\))|(\(L\)|\(l\))|(\(M\)|\(m\))|(\(N\))|(\(O\)|\(o\))|(\(P\)|\(p\))|(\(S\))|(\(T\)|\(t\))|(\(U\)|\(u\))|(\(W\)|\(w\))|(\(X\)|\(x\))|(\(Y\))|(\(Z\)|\(z\))|(\(6\))|(\(8\))|(\({\))|(\(}\))|(\(~\))|(\(@\))|(\(\*\))|(\(\^\))|(:-\[)|(:-\)|:\))|(:-D)|(:-O)|(:-P)|(:-\(|:\()|(:-S)|(:-\||:\|)|(:'\()|(:$|:-$)|(:-@|:@)|(;-\)|;\))|(\(n\))|(\(y\))|(\(Bn\))|(\(By\))|(%%-)";
        private static Regex findEmoticon = new Regex(findEmoticonString, RegexOptions.Compiled);

        private static string findIncludedTopicsString = @"((?<LineStart>\<Para\>\<IncludedTopic\>\{\{)(?<IncludedTopic>[^\}\r\n]+)(?<LineEnd>\}\}\</IncludedTopic\>\</Para\>))";
        private static Regex findIncludedTopic = new Regex(findIncludedTopicsString, RegexOptions.Compiled);

        private static string findWikiBehaviorString = @"((?<LineStart>^[^@(\r\n|\n)]*?)\<WikiTalkString\>@@(?<Behavior>(?<=@{2})[^@]+?(?=@{2}))@@\</WikiTalkString\>(?!"""")(?<LineEnd>.*?(\r\n|\n)))";
        private static Regex findWikiBehavior = new Regex(findWikiBehaviorString, RegexOptions.Compiled | RegexOptions.Multiline);

        private static string preBehaviorString = @"(^ {1}[^@\{\|\}]*)|(\r\n\t[^1\*])";
        private static Regex preBehavior = new Regex(preBehaviorString, RegexOptions.Multiline | RegexOptions.Compiled);

        private static string findAttachString = @"(?<StartAttach>\<WikiAttach\>)(?<linkData>.*)(?<EndAttach>\</WikiAttach\>)";
        private static Regex findAttach = new Regex(findAttachString, RegexOptions.Compiled);

        private static string insideTableString = @"\|\|\}|\{\|\|\}|\{\|\|";
        private static Regex insideTable = new Regex(insideTableString, RegexOptions.Compiled);

        private static Regex escAmpersand = new Regex(@"(&(?!amp;|#|nbsp;|lt;|gt;))", RegexOptions.Compiled);
        private static Regex escLeftAngle = new Regex(@"<", RegexOptions.Compiled);
        private static Regex escRightAngle = new Regex(@">", RegexOptions.Compiled);

        //private static readonly Lazy<FileLogManager> lazyFileLogManager = new Lazy<FileLogManager>(() => { DoSomeInitialization(); return new FileLogManager(); }, true);
        private static readonly Lazy<ParserEngine> _instance
            = new Lazy<ParserEngine>(() => { return new ParserEngine(); }, true);

        private ParserEngine()
        {
        }

        public static void Initialize()
        {
            //_appPath = HostingEnvironment.ApplicationPhysicalPath + "App_Data";
            //_appPath = @"C:\LynxWiki-root\LynxWiki\App_Data";
            _appPath = ConfigurationManager.AppSettings["HostWikiPath"] + "App_Data";
            Regex.CacheSize = 250;
            _context = new ParserContext("WikiText", GetMainPath, false);
            //_xslt = new XslCompiledTransform(true); //debug enabled for stylesheet during development
            _xslt = new XslCompiledTransform(true);
            _xslt.Load(XsltPath);
        }
        public static string AppPath
        {
            get { return _appPath; }
        }
        public static string GetMainPath
        {
            get { return Path.Combine(_appPath, "womContext.xml"); }
        }

        public static string GrammarMultiLinePath
        {
            get { return Path.Combine(_appPath, "womMultiLine.xml"); }
        }
        public static string GrammarIncludeTopicPath
        {
            get { return Path.Combine(_appPath, "womIncludeTopic.xml"); }
        }
        public static string GrammarWomTextPath
        {
            get { return Path.Combine(_appPath, "womText.xml"); }
        }
        public static string GrammarWomCellPath
        {
            get { return Path.Combine(_appPath, "womCell.xml"); }
        }
        public static string GrammarWomTableStylePath
        {
            get { return Path.Combine(_appPath, "womTableStyle.xml"); }
        }
        public static string GrammarWomStyledCodePath
        {
            get { return Path.Combine(_appPath, "womStyledCode.xml"); }
        }
        public static string GrammarWomStyledTextPath
        {
            get { return Path.Combine(_appPath, "womStyledText.xml"); }
        }
        public static string XsltPath
        {
            get { return Path.Combine(_appPath, "LynxWikiWeb.xslt"); }
        }
        //public WikiInputDocument WikiInputDocument
        //{
        //    get { return _wikiInputDoc; }
        //    set { _wikiInputDoc = value; }
        //}
        public GrammarDocument InitGrammarDocument(string path)
        {
            return new GrammarDocument(path);
        }
        public WomDocument WomDocument
        {
            get { return _womDocument; }
        }

        public static ParserEngine Engine
        {
            get { return _engine; }
        }
        //The ParserEngine can return different formats depending upon the xslt document used
        public XsltDocument XsltDoc(string path)
        {
            return _xsltDoc = new XsltDocument(path);
        }
        public static string FormattedTopic(string wikiText, string wikiDir = "", Dictionary<string, string> properties = null)
        {
            //initial version does not handle diffs
            string interpreted = "";
            //_mgr = _fed.NamespaceManagerForNamespace(topic.Namespace);
            WomDocument.ResetTableOfContents();
            WomDocument.anchors = new string[25];
            //_processBehaviorToText = true;
            //_behaviorTopic = topic;

            string wom = "";

            if (!String.IsNullOrEmpty(wom))
            {
                WomDocument xmldoc = new WomDocument(wikiDir, properties);
                //wom = findWikiBehavior.Replace(wom, new MatchEvaluator(wikiBehaviorMatch));
                interpreted = xmldoc.ParsedDocument;
                xmldoc = null;
            }
            else
            {
               wikiText = wikiText + "\r\n";

                wikiText = escape(wikiText);
                int size = 0;
                while (String.IsNullOrEmpty(interpreted))
                {
                    try
                    {
                        size = _chunk;

                        WomDocument xmldoc = ProcessText(wikiText, size, false, wikiDir, properties);

                        interpreted = xmldoc.ParsedDocument;
                        xmldoc = null;
                    }
                    catch (XmlException ex)
                    {
                        string error = ex.ToString();
                    }
                }
            }
            return interpreted;
        }
        //this is the main body where the real parsing work is done < 30 lines of code
        public static WomDocument ProcessText(string wikiInput, int size, bool fragment = false, string wikiDir = "", Dictionary<string, string> properties = null)
        {
            //_stopwatch = Stopwatch.StartNew();
            ParserContext savedcontext;
            bool redo = false;
            wikiInput = wikiInput.Replace("\r\n\r\n", "\r\n");
            wikiInput = wikiInput.Replace("        ", "\t");
            while (wikiInput.EndsWith("\r\n"))
            {
                wikiInput = wikiInput.Remove(wikiInput.LastIndexOf("\r\n"));
            }
            _womDocument = new WomDocument(wikiDir, properties);
            _womDocument.FragmentOnly = fragment;
            _womDocument.Begin();
            
            int chunk = _chunk;  //set the initial chunk size (for cache use)
            if (size > 0)
            {
                chunk = size;
            }
            chunkcnt = 1;
            while (_context.ParentRule != null)
            {
                if (_context.ParentRule.ParentContext != null)
                {
                    _context = _context.ParentRule.ParentContext;
                }
            }
            if (!String.IsNullOrEmpty(wikiInput))
            {
                StringBuilder source = new StringBuilder();
                StringBuilder temp = new StringBuilder();
                source.AppendLine(externalWikiRef.Replace(wikiInput, new MatchEvaluator(externalWikiRefMatch)));
                temp.Append(multilinePre.Replace(source.ToString(), new MatchEvaluator(multilinePreMatch)));
                source = temp;
                string savedtemp = temp.ToString();
                MatchCollection matches;
                while (source.Length > 0)
                {
                    string womElement = _context.WomElement;
                    //optimize here by passing in less than the full string when source is very long
                    //this gives a 5 times performance improvement
                    int matchcnt = 0;

                    if (source.Length > chunk * chunkcnt)
                    {
                        matches = _context.RegExp.Matches(source.ToString(0, chunk * chunkcnt));
                        matchcnt = matches.Count;
                    }
                    else
                    {
                        matches = _context.RegExp.Matches(source.ToString());
                        matchcnt = matches.Count;
                    }
                    if (matchcnt > 0)
                    {
                        if (matches[0].Index > 0)  //if (matches[0].Index > 0)
                        {
                            _womDocument.SimpleAdd(source.ToString(0, matches[0].Index), womElement, "", _context.RuleList[0]);
                        }
                        int x = 1;
                        if (_context.RegExpStr.StartsWith("(?:") && !matches[0].Value.StartsWith("%"))
                        {
                            x = 0;
                        }
                        int cnt = matches[0].Groups.Count;
                        for (int i = x; i < cnt; i++)
                        {
                            if (matches[0].Groups[i].Success)
                            {
                                if (womElement != "WikiText")
                                {
                                    //we are in a child rule set with an end condition
                                    //whereas WikiText ends by running out of source or matches
                                    if (i == 1)
                                    {
                                        i = 0;
                                    }
                                }
                                if (_womDocument.InTable && _womDocument.InItem && matches[0].Value.StartsWith("%"))
                                {
                                    i++;  //add one to the index here as we are in womText rules twice for this condition
                                }

                                ParserRule rule = _context.RuleList[i];
                                savedcontext = _context;
                                string addElement;
                                if (!String.IsNullOrEmpty(rule.WomElement))
                                {
                                    addElement = rule.WomElement;
                                }
                                else
                                {
                                    addElement = womElement;
                                }
                                _womDocument.SimpleAdd(matches[0].Value, addElement, rule.Jump, rule);
                                if (_womDocument.ParsedDocument.Contains("<<"))  //an error occurred
                                {
                                    chunkcnt++; //increase the chunk size and redo
                                    redo = true;
                                }
                                else
                                {
                                    if (addElement != "WikiStylingEnd")
                                    {
                                        _context = rule.Context;
                                    }
                                    //still in a line - only pop one context item
                                    else if (matches[0].Value == "%%" || matches[0].Value == "%" || matches[0].Value.Contains("{||"))  
                                    {
                                        _context = _context.ParentRule.ParentContext;
                                    }
                                    else //done with that line - pop all context back to start
                                    {
                                        while (_context.ParentRule != null)
                                        {
                                            if (_context.ParentRule.ParentContext != null)
                                            {
                                                _context = _context.ParentRule.ParentContext;
                                            }
                                        }
                                    }
                                }
                                break;
                            }                      
                        }
                        if (!redo) //an error occurred
                        {
                            bool modifyRemove = false;

                            if (womElement == "womListText" && matches[0].Value.Contains("{||")) //need to leave this bit in source
                            {
                                modifyRemove = true;
                            }
                            else if (womElement == "womWikiStyledText" && matches[0].Value == "%")
                            {
                                modifyRemove = true;
                            }

                            if (modifyRemove)
                            {
                                source.Remove(0, matches[0].Index);
                            }
                            else
                            {
                                source.Remove(0, matches[0].Index + matches[0].Length);
                            }
                        }
                        else
                        {
                            //reset and start over with larger chunk
                            source = new StringBuilder();
                            source.Append(savedtemp);
                            _womDocument = new WomDocument(wikiDir, properties);
                            _womDocument.Begin();
                            redo = false;
                            while (_context.ParentRule != null)
                            {
                                if (_context.ParentRule.ParentContext != null)
                                {
                                    _context = _context.ParentRule.ParentContext;
                                }
                            }
                        }
                    }
                    else
                    {
                        if (source.Length > chunk * chunkcnt) //no match in that chunk, increase size and retry
                        {
                            source = new StringBuilder();
                            source.Append(savedtemp);
                            _womDocument = new WomDocument(wikiDir, properties);
                            _womDocument.Begin();
                            chunkcnt++;
                            while (_context.ParentRule != null)
                            {
                                if (_context.ParentRule.ParentContext != null)
                                {
                                    _context = _context.ParentRule.ParentContext;
                                }
                            }
                        }
                        else
                        {
                            _womDocument.SimpleAdd(source.ToString(), womElement, "", _context.RuleList[0]);
                            source.Length = 0;
                        }
                    }
                }
                source = null;
                temp = null;
            }
            _womDocument.End();
            bool disableWikiEmoticons = true;
            bool disableTopicEmoticons = false; //this must be overridden to to true in a topic property DisableTopicProperties:
            if (HttpContext.Current.Application["__DisableWikiEmoticons"] != null) {
                disableWikiEmoticons = (bool)HttpContext.Current.Application["__DisableWikiEmoticons"];
            }
            if (_womDocument.Properties != null && _womDocument.Properties.ContainsKey("DisableTopicEmoticons")) {
                disableTopicEmoticons = Convert.ToBoolean(_womDocument.Properties["DisableTopicEmoticons"]);
            }
            if ((disableWikiEmoticons == false) && (disableTopicEmoticons == false))
            {
                MatchCollection emoticonMatches = findEmoticon.Matches(_womDocument.ParsedDocument);
                if (emoticonMatches.Count > 0)
                {
                    _womDocument.ConvertEmoticons(emoticonMatches);
                }
            }
            //_stopwatch.Stop();
            return _womDocument;
        }

        public ParserContext EngineContext
        {
            get { return _context; }
        }
        public ParserEngine ParserApplication
        {
            get { return this; }
        }
        private static string multilinePreMatch(Match match)
        {
            StringBuilder result = new StringBuilder();
            result.AppendLine(@"
<PreformattedMultilineKeyed>");
            result.AppendLine(match.Groups["PreText"].Value);
            result.AppendLine(@"</PreformattedMultilineKeyed>");
            return result.ToString();
        }
        private static string externalWikiRefMatch(Match match)
        {
            // match.Value may contain some additional leading and/or trailing white space that we don't want to lose.
            string result = match.Value;
            if (!emailAddress.IsMatch(result))
            {
                // Build a string containing the actual match.
                string noSpace = match.Groups["param"].Value + "@" + match.Groups["behavior"].Value;
                // Replace that in the whole match with the external wiki ref macro.
                result = result.Replace(noSpace, "@@InterWiki(\"$behavior\", \"$param\", \"$param\")@@");
                // Replace the parameters with the regex matches.
                result = result.Replace("$behavior", match.Groups["behavior"].Value).Replace("$param", match.Groups["param"].Value);
            }
            return result;
        }

        private static string wikiBehaviorMatch(Match match, string wikiDir = "", Dictionary<string, string> properties = null)
        {
            string replacement = match.ToString();
            string linestart = match.Groups["LineStart"].Value;
            string lineend = match.Groups["LineEnd"].Value;
            string expr = match.Groups["Behavior"].Value;
            match = null;

            bool doBehavior = false;
            if (insideTable.IsMatch(linestart)) {
                doBehavior = true;
            }
            else if (!preBehavior.IsMatch(linestart)) {
                doBehavior = true;
            }
            if (doBehavior) {
                WikiScriptEngine.Initialize();
                replacement = WikiScriptEngine.ExecuteBehavior(expr);
                if (!string.IsNullOrEmpty(replacement)) {
                    WomDocument _wom = ProcessText(replacement, _chunk, true, wikiDir, properties);
                    replacement = _wom.ParsedDocument;
                }
            }
            return replacement;
        }

        private static string wikiIncludedTopic(Match match)
        {
            string replacement = match.ToString();
            string linestart = match.Groups["LineStart"].Value;
            string lineend = match.Groups["LineEnd"].Value;
            string expr = match.Groups["IncludedTopic"].Value;
            string path = expr.Substring(0, expr.LastIndexOf("/")).Replace("/", "\\");
            string topic = expr.Substring(expr.LastIndexOf("/") + 1);
            
            // _intermediate.AppendFormat("<{0} level=\"{1}\">\r\n", womElement, text.Length);
            //_intermediate.AppendFormat("<{0}>{1}</{0}><{2}>{3}</{2}>", womElement, text, "AnchorText", anchor);

            WikiTopicRepository wtr = new WikiTopicRepository(Path.Combine(_appPath, path));
            replacement = string.Format("<{0} level=\"2\">\r\n<{1}>{2}</{1}></{0}>\r\n{3}", "Header", "womHeaderText", topic, wtr.GetCurrentVersion(topic).Body);

            return replacement;
        }

        private static string wikiAttach(Match match)
        {
            string replacement = match.ToString();
            string start = string.Empty;
            string linkData = match.Groups["linkData"].Value;
            string end = string.Empty;
            string ns = string.Empty;
            string file = string.Empty;
            if (linkData.Contains("upload/")) {
                int nsStart = linkData.LastIndexOf("upload/") + 7;
                int nsEnd = linkData.IndexOf("/", nsStart + 1);
                ns = linkData.Substring(nsStart, nsEnd - nsStart);
                int fileStart = 0;
                if (ns == "docs" || ns == "misc" || ns == "images" || ns == "videos") {
                    fileStart = nsEnd + 1;
                }
                else {
                    fileStart = linkData.IndexOf("/", nsEnd + 1) + 1;
                }
                int fileEnd = linkData.LastIndexOf("</");
                file = linkData.Substring(fileStart, fileEnd - fileStart);
            }
            IPrincipal user = HttpContext.Current.User;
            AttachmentRepository ar = new AttachmentRepository();
            AttachmentVersion av = ar.GetCurrentVersion(file, ns);
            bool authOK = false;

            if (ns == "docs" || ns == "misc" || ns == "images" || ns == "videos") {
                authOK = true;
            }
            else if (av != null) {
                authOK = av.UserReadAccess(user);
            }
            if (authOK) {
                start = @"<WikiAttachLink>";
                end = @"</WikiAttachLink>";
            }
            else {
                linkData = file;
            }
            return start + linkData + end;
        }

        //public string ErrorMessage(string title, string body)
        //{
        //    // We can't use the fancy ErrorString method -- it didn't exist in v0
        //    string nOut;
        //    nOut.WriteErrorMessage(title, body);
        //    return nOut;
        //}
        public static string WikiToPresentation(XmlDocument doc)
        {
            //return NestedFormat(s, Output);
            StringBuilder stringBuilder = new StringBuilder();
            StringWriter sw = new StringWriter(stringBuilder);
            XhtmlTextWriter writer = new XhtmlTextWriter(sw);
            _xslt.Transform(doc, null, writer);

            return writer.InnerWriter.ToString();
        }
        public static string WikiToPresentation(string s, string wikiDir = "", Dictionary<string, string> properties = null)
        {
            XmlDocument doc = new XmlDocument();
            // fix for scripting
            while (findIncludedTopic.IsMatch(s)) {
                s = findIncludedTopic.Replace(s, new MatchEvaluator(wikiIncludedTopic));
            }
            while (findWikiBehavior.IsMatch(s)) {
                s = findWikiBehavior.Replace(s, new MatchEvaluator(delegate(Match match) { return wikiBehaviorMatch(match, wikiDir, properties); }));
            }
            while (findAttach.IsMatch(s)) {
                s = findAttach.Replace(s, new MatchEvaluator(wikiAttach));
            }
            try {
                doc.LoadXml(s);
                
                return WikiToPresentation(doc);
            }
            catch (Exception ex) {
                return ex.Message;
            }
        }
        static public string escape(string input)
        {
            if (input == null)
                return "";
            // replace HTML special characters with character entities
            // this has the side-effect of stripping all markup from text
            string str = input;
            str = escAmpersand.Replace(str, "&amp;");
            //str = Regex.Replace(str, "\"", "&quot;");
            str = escLeftAngle.Replace(str, "&lt;");
            str = escRightAngle.Replace(str, "&gt;");
            return str;
        }
    }
}
